<?php

namespace Xehang\AliyunOss;

use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use OSS\OssClient;
use OSS\Core\OssException;

class AliyunOssAdapter implements AdapterInterface
{
    /**
     * oss client
     *
     * @var \OSS\OssClient
     */
    protected $client;

    /**
     * @var string
     */
    protected $bucket;

    /**
     * @var string
     */
    protected $endpoint;

    /**
     * @var string
     */
    protected $domain;

    /**
     * @var bool
     */
    protected $ssl;

    protected static $metaOptions = [
        OssClient::OSS_CONTENT_TYPE,
        OssClient::OSS_CONTENT_LENGTH,
        OssClient::OSS_CACHE_CONTROL,
        OssClient::OSS_EXPIRES,
        OssClient::OSS_CONTENT_DISPOSTION,
        OssClient::OSS_CHECK_MD5,
        OssClient::OSS_OBJECT_ACL,
        OssClient::OSS_ENCODING_TYPE,
    ];

    /**
     * constructor
     * @param \OSS\OssClient $client
     * @param string $bucket
     * @param string $endpoint
     * @param bool   $ssl
     * @param string $domain
     */
    public function __construct($client, $bucket, $endpoint, $domain, $ssl)
    {
        $this->client = $client;
        $this->bucket = $bucket;
        $this->endpoint = $endpoint;
        $this->domain = $domain;
        $this->ssl = $ssl;
        $this->client->setUseSSL($ssl);
    }

    /**
     * get the OssClient
     *
     * @return \OSS\OssClient
     */
    public function getClient()
    {
        return $this->client;
    }

    /**
     * returns the url to the file / dir
     *
     * @param  $path
     * @return string
     */
    public function getUrl(string $path)
    {
        $scheme = $this->ssl ? 'https://' : 'http://';

        return $scheme . $this->domain . '/' . ltrim($path);
    }

    protected function getOptions(Config $config)
    {
        $options = [];

        foreach (static::$metaOptions as $option) {
            if (!$config->has($option)) {
                continue;
            }
            $options[$option] = $config->get($option);
        }

        if ($visibility = $config->get('visibility')) {
            if ($visibility === AdapterInterface::VISIBILITY_PUBLIC) {
                $options['x-oss-object-acl'] = OssClient::OSS_ACL_TYPE_PUBLIC_READ;
            } else {
                $options['x-oss-object-acl'] = OssClient::OSS_ACL_TYPE_PRIVATE;
            }
        }

        return [
            OssClient::OSS_HEADERS => $options,
        ];
    }

    /**
     * Write a new file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function write($path, $contents, Config $config)
    {
        $bucket = $this->bucket;
        $options = $this->getOptions($config);

        try {
            $this->client->putObject($bucket, $path, $contents, $options);
        } catch (OssException $e) {
            return false;
        }

        return ['type' => 'file', 'path' => $path, 'contents' => $contents];
    }

    /**
     * Write a new file using a stream.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function writeStream($path, $resource, Config $config)
    {
        $contents = stream_get_contents($resource);

        return $this->write($path, $contents, $config);
    }

    /**
     * Update a file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function update($path, $contents, Config $config)
    {
        return $this->write($path, $contents, $config);
    }

    /**
     * Update a file using a stream.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function updateStream($path, $resource, Config $config)
    {
        return $this->writeStream($path, $resource, $config);
    }

    /**
     * Rename a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function rename($path, $newpath)
    {
        if (!$this->copy($path, $newpath)) {
            return false;
        }

        return $this->delete($path);
    }

    /**
     * Copy a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function copy($path, $newpath)
    {
        $bucket = $this->bucket;

        try {
            $this->client->copyObject($bucket, $path, $bucket, $newpath);
        } catch (OssException $e) {
            return false;
        }

        return true;
    }

    /**
     * Delete a file.
     *
     * @param string $path
     *
     * @return bool
     */
    public function delete($path)
    {
        $bucket = $this->bucket;

        try {
            $this->client->deleteObject($bucket, $path);
        } catch (OssException $e) {
            return false;
        }

        return true;
    }

    /**
     * Delete a directory.
     *
     * @param string $dirname
     *
     * @return bool
     */
    public function deleteDir($dirname)
    {
        $bucket = $this->bucket;

        try {
            $paths = array_map(function ($object) {
                return $object['path'];
            }, $this->listContents($dirname, true));
            $this->client->deleteObjects($bucket, $paths);
        } catch (OssException $e) {
            return false;
        }

        return true;
    }

    /**
     * Create a directory.
     *
     * @param string $dirname directory name
     * @param Config $config
     *
     * @return array|false
     */
    public function createDir($dirname, Config $config)
    {
        $bucket = $this->bucket;
        $object = rtrim($dirname);

        try {
            $this->client->createObjectDir($bucket, $object);
        } catch (OssException $e) {
            return false;
        }

        return ['path' => $object, 'type' => 'dir'];
    }

    /**
     * Set the visibility for a file.
     *
     * @param string $path
     * @param string $visibility
     *
     * @return array|false file meta data
     */
    public function setVisibility($path, $visibility)
    {
        $object = $this->applyPathPrefix($path);

        if ($visibility === AdapterInterface::VISIBILITY_PUBLIC) {
            $acl = OssClient::OSS_ACL_TYPE_PUBLIC_READ;
        } else {
            $acl = OssClient::OSS_ACL_TYPE_PRIVATE;
        }

        $this->client->putObjectAcl($this->bucket, $object, $acl);

        return ['visibility' => $visibility];
    }

    /**
     * Check whether a file exists.
     *
     * @param string $path
     *
     * @return array|bool|null
     */
    public function has($path)
    {
        $bucket = $this->bucket;

        return $this->client->doesObjectExist($bucket, $path);
    }

    /**
     * Read a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function read($path)
    {
        $bucket = $this->bucket;

        try {
            $contents = $this->client->getObject($bucket, $path);
        } catch (OssException $e) {
            return false;
        }

        return ['contents' => $contents];
    }

    /**
     * Read a file as a stream.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function readStream($path)
    {
        $bucket = $this->bucket;

        $url = $this->getUrl($path);

        $stream = fopen($url, 'r');

        if (!is_resource($stream)) {
            return false;
        }

        return ['stream' => $stream];
    }

    /**
     * List contents of a directory.
     *
     * @param string $directory
     * @param bool   $recursive
     *
     * @return array
     */
    public function listContents($directory = '', $recursive = false)
    {
        $bucket = $this->bucket;
        $result = [];

        $delimiter = $recursive ? '' : '/';
        $prefix = $directory;
        $maxKeys = 1000;
        $nextMarker = '';

        while (true) {
            try {
                $options = [
                    'delimiter' => $delimiter,
                    'prefix'    => $prefix,
                    'max-keys'  => $maxKeys,
                    'marker'    => $nextMarker,
                ];
                $listInfo = $this->client->listObjects($bucket, $options);

                $prefixesInfo = $listInfo->getPrefixList();
                foreach ($prefixesInfo as $prefixInfo) {
                    $result[] = [
                        'type' => 'dir',
                        'path' => $prefixInfo->getPrefix(),
                    ];
                }

                $objectsInfo = $listInfo->getObjectList();
                foreach ($objectsInfo as $objectInfo) {
                    $path = $objectInfo->getKey();
                    $result[] = [
                        'type' => substr($path, -1) === '/' ? 'dir' : 'file',
                        'path' => $objectInfo->getKey(),
                    ];
                }

                $nextMarker = $listInfo->getNextMarker();
                if ($listInfo->getIsTruncated() !== 'true') {
                    break;
                }
            } catch (OssException $e) {
                return false;
            }
        }
        return $result;
    }

    /**
     * Get all the meta data of a file or directory.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMetadata($path)
    {
        $bucket = $this->bucket;

        try {
            $meta = $this->client->getObjectMeta($bucket, $path);
        } catch (OssException $e) {
            return false;
        }

        return $meta;
    }

    /**
     * Get the size of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getSize($path)
    {
        $object = $this->getMetadata($path);
        if ($object) {
            $object['size'] = $object['content-length'];
        }
        return $object;
    }

    /**
     * Get the mimetype of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMimetype($path)
    {
        $object = $this->getMetadata($path);
        if ($object) {
            $object['mimetype'] = $object['content-type'];
        }
        return $object;
    }

    /**
     * Get the last modified time of a file as a timestamp.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getTimestamp($path)
    {
        $object = $this->getMetadata($path);
        if ($object) {
            $object['timestamp'] = strtotime($object['last-modified']);
        }
        return $object;
    }

    /**
     * Get the visibility of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getVisibility($path)
    {
        $bucket = $this->bucket;

        try {
            $acl = $this->client->getObjectAcl($bucket, $path);
        } catch (OssException $e) {
            return false;
        }

        if ($acl == OssClient::OSS_ACL_TYPE_PUBLIC_READ) {
            $res['visibility'] = AdapterInterface::VISIBILITY_PUBLIC;
        } else {
            $res['visibility'] = AdapterInterface::VISIBILITY_PRIVATE;
        }

        return $res;
    }
}
